use pretty_assertions::assert_eq;
use psl::parser_database::{ExtensionTypes, NoExtensionTypes};
use schema_core::{
    CoreError, CoreResult, commands::create_migration, json_rpc::types::*, schema_connector::SchemaConnector,
};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use test_setup::runtime::run_with_thread_local_runtime;

use crate::utils;

pub struct CreateMigration<'a> {
    api: &'a mut dyn SchemaConnector,
    files: Vec<SchemaContainer>,
    migrations_directory: &'a TempDir,
    draft: bool,
    name: &'a str,
    filter: SchemaFilter,
    init_script: &'a str,
    extension_types: &'a dyn ExtensionTypes,
}

/// The file name for migration scripts, not including the file extension.
const MIGRATION_SCRIPT_FILENAME: &str = "migration";

/// The file name for the migration lock file, not including the file extension.
const MIGRATION_LOCK_FILENAME: &str = "migration_lock";

/// Proxy to a directory containing one migration, as returned by
/// `create_migration_directory` and `list_migrations`.
#[derive(Debug, Clone)]
pub struct MigrationDirectory {
    path: PathBuf,
}

impl MigrationDirectory {
    /// Write the migration script to the directory.
    pub fn write_migration_script(&self, script: &str, extension: &str) -> std::io::Result<()> {
        let mut path = self.path.join(MIGRATION_SCRIPT_FILENAME);

        path.set_extension(extension);

        tracing::debug!("Writing migration script at {:?}", &path);
        std::fs::write(path, script)?;

        Ok(())
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

/// Create a directory for a new migration.
pub fn create_migration_directory(
    migrations_directory_path: &Path,
    migration_name: &str,
) -> std::io::Result<MigrationDirectory> {
    let directory_path = migrations_directory_path.join(migration_name);

    if directory_path.exists() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::AlreadyExists,
            format!(
                "The migration directory already exists at {}",
                directory_path.to_string_lossy()
            ),
        ));
    }

    std::fs::create_dir_all(&directory_path)?;

    Ok(MigrationDirectory { path: directory_path })
}

/// Write the migration_lock file to the directory.
pub fn write_migration_lock_file(migrations_directory_path: &Path, provider: &str) -> std::io::Result<()> {
    let mut file_path = migrations_directory_path.join(MIGRATION_LOCK_FILENAME);

    file_path.set_extension("toml");

    tracing::debug!("Writing migration lockfile at {:?}", &file_path);

    let content = format!(
        r##"# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "{provider}""##
    );

    std::fs::write(file_path, &content)?;

    Ok(())
}

impl<'a> CreateMigration<'a> {
    pub fn new(
        api: &'a mut dyn SchemaConnector,
        name: &'a str,
        files: &[(&'a str, &'a str)],
        migrations_directory: &'a TempDir,
        filter: SchemaFilter,
        init_script: &'a str,
    ) -> Self {
        CreateMigration {
            api,
            files: files
                .iter()
                .map(|(path, content)| SchemaContainer {
                    path: path.to_string(),
                    content: content.to_string(),
                })
                .collect(),
            migrations_directory,
            draft: false,
            name,
            filter,
            init_script,
            extension_types: &NoExtensionTypes,
        }
    }

    pub fn draft(mut self, draft: bool) -> Self {
        self.draft = draft;

        self
    }

    pub fn extension_types(mut self, extension_types: &'a dyn ExtensionTypes) -> Self {
        self.extension_types = extension_types;
        self
    }

    pub async fn send(self) -> CoreResult<CreateMigrationAssertion<'a>> {
        let mut migrations_list = utils::list_migrations(self.migrations_directory.path()).unwrap();
        migrations_list.shadow_db_init_script = self.init_script.to_string();
        let migration_name = self.name.to_owned();
        let mut migration_schema_cache = Default::default();
        let output = create_migration(
            CreateMigrationInput {
                migrations_list,
                schema: SchemasContainer { files: self.files },
                draft: self.draft,
                migration_name: migration_name.clone(),
                filters: self.filter,
            },
            self.api,
            &mut migration_schema_cache,
            self.extension_types,
        )
        .await?;

        if let Some(migration_script) = &output.migration_script {
            let directory =
                create_migration_directory(self.migrations_directory.path(), &output.generated_migration_name)
                    .map_err(|_| CoreError::from_msg("Failed to create a new migration directory.".into()))?;

            // Write the migration script to a file.
            directory
                .write_migration_script(migration_script, "sql")
                .map_err(|err| {
                    CoreError::from_msg(format!(
                        "Failed to write the migration script to `{:?}`\n{}",
                        directory.path(),
                        err,
                    ))
                })?;

            write_migration_lock_file(self.migrations_directory.path(), &output.connector_type).map_err(|err| {
                CoreError::from_msg(format!(
                    "Failed to write the migration lock file to `{:?}`\n{}",
                    self.migrations_directory.path(),
                    err
                ))
            })?;
        }

        Ok(CreateMigrationAssertion {
            generated_migration_name: output.generated_migration_name.clone(),
            output,
            migrations_directory: self.migrations_directory,
        })
    }

    #[track_caller]
    pub fn send_sync(self) -> CreateMigrationAssertion<'a> {
        run_with_thread_local_runtime(self.send()).unwrap()
    }

    #[track_caller]
    pub fn send_unwrap_err(self) -> CoreError {
        run_with_thread_local_runtime(self.send()).unwrap_err()
    }
}

pub struct CreateMigrationAssertion<'a> {
    pub output: CreateMigrationOutput,
    migrations_directory: &'a TempDir,
    generated_migration_name: String,
}

impl std::fmt::Debug for CreateMigrationAssertion<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "CreateMigrationAssertion {{ .. }}")
    }
}

impl CreateMigrationAssertion<'_> {
    /// Assert that there are `expected_count` migrations in the migrations directory.
    #[tracing::instrument(skip(self))]
    #[track_caller]
    pub fn assert_migration_directories_count(self, expected_count: usize) -> Self {
        let mut count = 0;

        for entry in
            std::fs::read_dir(self.migrations_directory.path()).expect("Counting directories in migrations directory.")
        {
            let entry = entry.unwrap();

            if entry.path().file_name().and_then(|s| s.to_str()) == Some("migration_lock.toml") {
                continue;
            }

            count += 1;
        }

        assert!(
            // the lock file is counted as an entry
            expected_count == count,
            "Assertion failed. Expected {expected_count} migrations in the migrations directory, found {count}."
        );

        self
    }

    /// Assert that there is one migration with `name_matcher` contained in its name present in the migration directory.
    pub fn assert_migration<F>(self, name_matcher: &str, assertions: F) -> Self
    where
        F: for<'b> FnOnce(MigrationAssertion<'b>) -> MigrationAssertion<'b>,
    {
        let migration = std::fs::read_dir(self.migrations_directory.path())
            .expect("Reading migrations directory for named migration.")
            .find_map(|entry| {
                let entry = entry.unwrap();
                let name = entry.file_name();

                if name.to_str().unwrap().contains(name_matcher) {
                    Some(entry)
                } else {
                    None
                }
            });

        match migration {
            Some(migration) => {
                let path = migration.path();
                let assertion = MigrationAssertion { path: path.as_ref() };

                assertions(assertion);
            }
            None => panic!("Assertion error. Could not find migration with name matching `{name_matcher}`"),
        }

        self
    }

    pub fn output(&self) -> &CreateMigrationOutput {
        &self.output
    }

    pub fn migration_script_path(&self) -> PathBuf {
        self.migrations_directory
            .path()
            .join(&self.generated_migration_name)
            .join("migration.sql")
    }

    #[track_caller]
    pub fn modify_migration<F>(self, modify: F) -> Self
    where
        F: FnOnce(&mut String),
    {
        use std::io::Write as _;

        let migration_script_path = self.migration_script_path();
        let new_contents = {
            let mut contents = std::fs::read_to_string(&migration_script_path).expect("Reading migration script");

            modify(&mut contents);

            contents
        };

        let mut file = std::fs::File::create(&migration_script_path).unwrap();
        write!(file, "{new_contents}").unwrap();

        self
    }

    pub fn into_output(self) -> CreateMigrationOutput {
        self.output
    }
}

pub struct MigrationAssertion<'a> {
    path: &'a Path,
}

impl MigrationAssertion<'_> {
    #[track_caller]
    pub fn expect_contents(self, expected_contents: expect_test::Expect) -> Self {
        let migration_file_path = self.path.join("migration.sql");
        let contents: String = std::fs::read_to_string(&migration_file_path)
            .map_err(|_| format!("Trying to read migration file at {migration_file_path:?}"))
            .unwrap();

        expected_contents.assert_eq(&contents);
        self
    }

    #[track_caller]
    pub fn assert_contents(self, expected_contents: &str) -> Self {
        let migration_file_path = self.path.join("migration.sql");
        let contents: String = std::fs::read_to_string(&migration_file_path)
            .map_err(|_| format!("Trying to read migration file at {migration_file_path:?}"))
            .unwrap();

        assert_eq!(expected_contents, contents);
        self
    }
}
